// Copyright (c) 2017 Uber Technologies, Inc.
//

import _get from 'lodash/get'
import type React from 'react'

import EUpdateTypes from './EUpdateTypes'
import { DraggableBounds, DraggingUpdate } from './types'
import { TNil } from 'types/misc'

const LEFT_MOUSE_BUTTON = 0

type DraggableManagerOptions = {
  getBounds: (tag: string | TNil) => DraggableBounds
  onMouseEnter?: (update: DraggingUpdate) => void
  onMouseLeave?: (update: DraggingUpdate) => void
  onMouseMove?: (update: DraggingUpdate) => void
  onDragStart?: (update: DraggingUpdate) => void
  onDragMove?: (update: DraggingUpdate) => void
  onDragEnd?: (update: DraggingUpdate) => void
  resetBoundsOnResize?: boolean
  tag?: string
}

export default class DraggableManager {
  // cache the last known DraggableBounds (invalidate via `#resetBounds())
  _bounds: DraggableBounds | TNil
  _isDragging: boolean
  // optional callbacks for various dragging events
  _onMouseEnter: ((update: DraggingUpdate) => void) | TNil
  _onMouseLeave: ((update: DraggingUpdate) => void) | TNil
  _onMouseMove: ((update: DraggingUpdate) => void) | TNil
  _onDragStart: ((update: DraggingUpdate) => void) | TNil
  _onDragMove: ((update: DraggingUpdate) => void) | TNil
  _onDragEnd: ((update: DraggingUpdate) => void) | TNil
  // whether to reset the bounds on window resize
  _resetBoundsOnResize: boolean

  /**
   * Get the `DraggableBounds` for the current drag. The returned value is
   * cached until either `#resetBounds()` is called or the window is resized
   * (assuming `_resetBoundsOnResize` is `true`). The `DraggableBounds` defines
   * the range the current drag can span to. It also establishes the left offset
   * to adjust `clientX` by (from the `MouseEvent`s).
   */
  getBounds: (tag: string | TNil) => DraggableBounds

  // convenience data
  tag: string | TNil

  // handlers for integration with DOM elements
  handleMouseEnter: (event: React.MouseEvent<any>) => void
  handleMouseMove: (event: React.MouseEvent<any>) => void
  handleMouseLeave: (event: React.MouseEvent<any>) => void
  handleMouseDown: (event: React.MouseEvent<any>) => void

  constructor({
    getBounds,
    tag,
    resetBoundsOnResize = true,
    ...rest
  }: DraggableManagerOptions) {
    this.handleMouseDown = this._handleDragEvent
    this.handleMouseEnter = this._handleMinorMouseEvent
    this.handleMouseMove = this._handleMinorMouseEvent
    this.handleMouseLeave = this._handleMinorMouseEvent

    this.getBounds = getBounds
    this.tag = tag
    this._isDragging = false
    this._bounds = undefined
    this._resetBoundsOnResize = Boolean(resetBoundsOnResize)
    if (this._resetBoundsOnResize) {
      window.addEventListener('resize', this.resetBounds)
    }
    this._onMouseEnter = rest.onMouseEnter
    this._onMouseLeave = rest.onMouseLeave
    this._onMouseMove = rest.onMouseMove
    this._onDragStart = rest.onDragStart
    this._onDragMove = rest.onDragMove
    this._onDragEnd = rest.onDragEnd
  }

  _getBounds(): DraggableBounds {
    if (!this._bounds) {
      this._bounds = this.getBounds(this.tag)
    }
    return this._bounds
  }

  _getPosition(clientX: number) {
    const { clientXLeft, maxValue, minValue, width } = this._getBounds()
    let x = clientX - clientXLeft
    let value = x / width
    if (minValue != null && value < minValue) {
      value = minValue
      x = minValue * width
    } else if (maxValue != null && value > maxValue) {
      value = maxValue
      x = maxValue * width
    }
    return { value, x }
  }

  _stopDragging() {
    window.removeEventListener('mousemove', this._handleDragEvent)
    window.removeEventListener('mouseup', this._handleDragEvent)
    const style = _get(document, 'body.style')
    if (style) {
      style.userSelect = null
    }
    this._isDragging = false
  }

  isDragging() {
    return this._isDragging
  }

  dispose() {
    if (this._isDragging) {
      this._stopDragging()
    }
    if (this._resetBoundsOnResize) {
      window.removeEventListener('resize', this.resetBounds)
    }
    this._bounds = undefined
    this._onMouseEnter = undefined
    this._onMouseLeave = undefined
    this._onMouseMove = undefined
    this._onDragStart = undefined
    this._onDragMove = undefined
    this._onDragEnd = undefined
  }

  resetBounds = () => {
    this._bounds = undefined
  }

  _handleMinorMouseEvent = (event: React.MouseEvent<any>) => {
    const { button, clientX, type: eventType } = event
    if (this._isDragging || button !== LEFT_MOUSE_BUTTON) {
      return
    }
    let type: EUpdateTypes | null = null
    let handler: ((update: DraggingUpdate) => void) | TNil
    if (eventType === 'mouseenter') {
      type = EUpdateTypes.MouseEnter
      handler = this._onMouseEnter
    } else if (eventType === 'mouseleave') {
      type = EUpdateTypes.MouseLeave
      handler = this._onMouseLeave
    } else if (eventType === 'mousemove') {
      type = EUpdateTypes.MouseMove
      handler = this._onMouseMove
    } else {
      throw new Error(`invalid event type: ${eventType}`)
    }
    if (!handler) {
      return
    }
    const { value, x } = this._getPosition(clientX)
    handler({
      event,
      type,
      value,
      x,
      manager: this,
      tag: this.tag,
    })
  }

  _handleDragEvent = (event: MouseEvent | React.MouseEvent<any>) => {
    const { button, clientX, type: eventType } = event
    let type: EUpdateTypes | null = null
    let handler: ((update: DraggingUpdate) => void) | TNil
    if (eventType === 'mousedown') {
      if (this._isDragging || button !== LEFT_MOUSE_BUTTON) {
        return
      }
      window.addEventListener('mousemove', this._handleDragEvent)
      window.addEventListener('mouseup', this._handleDragEvent)
      const style = _get(document, 'body.style')
      if (style) {
        style.userSelect = 'none'
      }
      this._isDragging = true

      type = EUpdateTypes.DragStart
      handler = this._onDragStart
    } else if (eventType === 'mousemove') {
      if (!this._isDragging) {
        return
      }
      type = EUpdateTypes.DragMove
      handler = this._onDragMove
    } else if (eventType === 'mouseup') {
      if (!this._isDragging) {
        return
      }
      this._stopDragging()
      type = EUpdateTypes.DragEnd
      handler = this._onDragEnd
    } else {
      throw new Error(`invalid event type: ${eventType}`)
    }
    if (!handler) {
      return
    }
    const { value, x } = this._getPosition(clientX)
    handler({
      event,
      type,
      value,
      x,
      manager: this,
      tag: this.tag,
    })
  }
}
